Gin 框架学习
Gin 框架是什么?
Gin 是 Go 的 Web 框架,项目地址 Gin
go get -u github.com/gin-gonic/gin
项目各个目录的编排
各个语言项目的目录结构都不一样,记录下这个项目的组织方式:
├─conf 用于存储配置文件
├─middleware 应用中间件
│ └─jwt
├─models 应用数据库模型
├─pkg 存放的是可供项目内部/外部所使用的公共性代码
│ ├─app
│ ├─e
│ ├─export
│ ├─file
│ ├─gredis
│ ├─logging
│ ├─qrcode
│ ├─setting
│ ├─upload
│ └─util
├─routers 路由逻辑处理
│ └─api
│ └─v1
├─runtime 应用运行时数据
│ ├─fonts
│ └─qrcode
├─service
│ ├─article_service
│ ├─auth_service
│ ├─cache_service
│ └─tag_service
└─vendor 第三方包依赖
快速使用
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run()
// listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}
这个 gin.Default()
返回 Gin 的 type Engine struct{...}
,里面包含 RouterGroup,相当于创建一个路由 Handlers,可以后期绑定各类的路由规则和函数、中间件等
这个 gin.H{…}
就是一个 map[string]interface{}
这个 gin.Context
Context 是 gin 中的上下文,它允许我们在中间件之间传递变量、管理流、验证 JSON 请求、响应 JSON 请求等,在 gin 中包含大量 Context 的方法,例如我们常用的 DefaultQuery、Query、DefaultPostForm、PostForm 等等
访问测试:
curl http://127.0.0.1:8080/ping
Context 的设计
Gin 使用 Context 封装了 Request、Response 这两个对象
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run()
// listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}
实际上底层(Gin 包的 gin.go
文件),Gin 内部维护了一个 Context 对象池,用来分配 Context,请求过来时动态的把 req、resp 对象传进去,至于全局污染怎么解决,也简单粗暴,直接在下一个连接过来的时候清空这个对象保存的状态,下图所示:
这个 Context 就是把一堆全局变量打包成一个对象,方便调用它的方法
这个设计在其它的系统中也很常见,可以参考知乎的这个问题 为什么那么多框架都设计有Context?
配置 Gin 框架
配置 Server 服务
func main() {
router := gin.Default()
s := &http.Server{
Addr: ":8080",
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()
}
这里的 http.ListenAndServe
和上面的 r.Run()
有区别吗?
点进里面,得知本质上没有区别
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine)
return
}
可以直接使用配置
func main() {
router := gin.Default()
http.ListenAndServe(":8080", router)
}
这个 http.Server
- Addr:监听的 TCP 地址,格式为:8000
- Handler:http 句柄,实质为ServeHTTP,用于处理程序响应 HTTP 请求
- TLSConfig:安全传输层协议(TLS)的配置
- ReadTimeout:允许读取的最大时间
- ReadHeaderTimeout:允许读取请求头的最大时间
- WriteTimeout:允许写入的最大时间
- IdleTimeout:等待的最大时间
- MaxHeaderBytes:请求头的最大字节数
- ConnState:指定一个可选的回调函数,当客户端连接发生变化时调用
- ErrorLog:指定一个可选的日志记录器,用于接收程序的意外行为和底层系统错误;如果未设置或为
nil
则默认以日志包的标准日志记录器完成(也就是在控制台输出)
控制日志输出颜色
默认情况下,控制台上输出的日志会根据检测到的 TTY 进行着色。
如果不想给日志着色,可以这么做:
func main() {
// 禁止日志颜色
gin.DisableConsoleColor()
// 使用默认中间件(logger 和 recovery)创建 gin 路由器
router := gin.Default()
// 定义路由
router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
// 启动服务器
router.Run(":8080")
}
如果强制给日志着色,可以这么做:
func main() {
// 强制设置日志颜色
gin.ForceConsoleColor()
router := gin.Default()
router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
router.Run(":8080")
}
自定义日志格式
func main() {
router := gin.New()
// LoggerWithFormatter 中间件会将日志写入 gin.DefaultWriter
// 默认情况下 gin.DefaultWriter 是 os.Stdout
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
// 自定义日志输出格式
return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
param.ClientIP,
param.TimeStamp.Format(time.RFC1123),
param.Method,
param.Path,
param.Request.Proto,
param.StatusCode,
param.Latency,
param.Request.UserAgent(),
param.ErrorMessage,
)
}))
// 使用 recovery 中间件
router.Use(gin.Recovery())
router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
router.Run(":8080")
}
将日志信息写入文件
Gin 框架默认将日志输出到控制台,要写入指定的日志文件,可以这么做
func main() {
// 创建日志文件并设置为 gin.DefaultWriter
f, _ := os.Create("gin.log")
gin.DefaultWriter = io.MultiWriter(f)
// 如果你需要同时写入日志文件和控制台,可以这么做:
// gin.DefaultWriter = io.MultiWriter(f, os.Stdout)
router := gin.Default()
router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
router.Run(":8080")
}
Cookie
func main() {
router := gin.Default()
router.GET("/cookie", func(c *gin.Context) {
// 读取 Cookie
cookie, err := c.Cookie("gin_cookie")
if err != nil {
cookie = "NotSet"
// 设置 Cookie
c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
}
fmt.Printf("Cookie value: %s \n", cookie)
})
router.Run(":8088")
}
运行上述代码,访问 /cookie
,在控制台日志输出中可以看到打印的 Cookie 信息:
Session
在 Gin 框架中,我们可以依赖 gin-contrib/sessions
中间件处理 session。
go get github.com/gin-contrib/sessions
基本的 session 用法
package main
import (
// 导入session包
"github.com/gin-contrib/sessions"
// 导入session存储引擎
"github.com/gin-contrib/sessions/cookie"
// 导入gin框架包
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// 创建基于 cookie 的存储引擎,secret11111 参数是用于加密的密钥
store := cookie.NewStore([]byte("secret11111"))
// 设置 session 中间件,参数 mysession,指的是 session 的名字,也是 cookie 的名字
// store 是前面创建的存储引擎,我们可以替换成其他存储引擎
r.Use(sessions.Sessions("mysession", store))
r.GET("/hello", func(c *gin.Context) {
// 初始化session对象
session := sessions.Default(c)
// 通过 session.Get 读取 session 值
// session是键值对格式数据,因此需要通过key查询数据
if session.Get("hello") != "world" {
// 设置 session 数据
session.Set("hello", "world")
// 删除 session 数据
session.Delete("mytag")
// 保存 session 数据
session.Save()
// 删除整个session
// session.Clear()
}
c.JSON(200, gin.H{"hello": session.Get("hello")})
})
r.Run(":8000")
}
Redis 存储 Session
如果我们想将 Session 数据保存到 Redis 中,只要将 Session 的存储引擎改成 Redis 即可。
go get github.com/gin-contrib/sessions/redis
使用例:
package main
import (
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/redis"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// 初始化基于redis的存储引擎
// 参数说明:
// 第1个参数 - redis最大的空闲连接数
// 第2个参数 - 数通信协议tcp或者udp
// 第3个参数 - redis地址, 格式,host:port
// 第4个参数 - redis密码
// 第5个参数 - session加密密钥
store, _ := redis.NewStore(10, "tcp", "localhost:6379", "", []byte("secret"))
r.Use(sessions.Sessions("mysession", store))
r.GET("/incr", func(c *gin.Context) {
session := sessions.Default(c)
var count int
v := session.Get("count")
if v == nil {
count = 0
} else {
count = v.(int)
count++
}
session.Set("count", count)
session.Save()
c.JSON(200, gin.H{"count": count})
})
r.Run(":8000")
}
同时启动多个服务
需要下载 Go 官方提供的扩展包 errgroup 可以多子进程
go get -u golang.org/x/sync/errgroup
使用这个拓展实现多个服务
import (
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
"golang.org/x/sync/errgroup"
)
var (
g errgroup.Group
)
func router01() http.Handler {
e := gin.New()
e.Use(gin.Recovery())
e.GET("/", func(c *gin.Context) {
c.JSON(
http.StatusOK,
gin.H{
"code": http.StatusOK,
"error": "Welcome server 01",
},
)
})
return e
}
func router02() http.Handler {
e := gin.New()
e.Use(gin.Recovery())
e.GET("/", func(c *gin.Context) {
c.JSON(
http.StatusOK,
gin.H{
"code": http.StatusOK,
"error": "Welcome server 02",
},
)
})
return e
}
func main() {
server01 := &http.Server{
Addr: ":8080",
Handler: router01(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
server02 := &http.Server{
Addr: ":8081",
Handler: router02(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
g.Go(func() error {
return server01.ListenAndServe()
})
g.Go(func() error {
return server02.ListenAndServe()
})
if err := g.Wait(); err != nil {
log.Fatal(err)
}
}
配置路由
可以抽离出路由这块代码
func InitRouter() *gin.Engine {
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())
gin.SetMode(setting.RunMode)
r.GET("/test", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "test",
})
})
return r
}
这样 main 里面就可以直接使用
func main() {
router := InitRouter()
http.ListenAndServe(":8080", router)
}
各种请求类型
// 创建带有默认中间件的路由:
// 日志与恢复中间件
router := gin.Default()
//创建不带中间件的路由:
//r := gin.New()
router.GET("/someGet", getting)
router.POST("/somePost", posting)
router.PUT("/somePut", putting)
router.DELETE("/someDelete", deleting)
router.PATCH("/somePatch", patching)
router.HEAD("/someHead", head)
router.OPTIONS("/someOptions", options)
路由群组
这个和 SpringMVC 那个配置了 Class 统一路由路径是一样的
func loginEndpoint(c *gin.Context) {
c.String(200, "login")
}
func submitEndpoint(c *gin.Context) {
c.String(200, "submit")
}
func readEndpoint(c *gin.Context) {
c.String(200, "read")
}
func main() {
router := gin.Default()
// Simple group: v1
v1 := router.Group("/v1")
{
v1.POST("/login", loginEndpoint)
v1.POST("/submit", submitEndpoint)
v1.POST("/read", readEndpoint)
}
// Simple group: v2
v2 := router.Group("/v2")
{
v2.POST("/login", loginEndpoint)
v2.POST("/submit", submitEndpoint)
v2.POST("/read", readEndpoint)
}
router.Run(":8080")
}
这样我们就以版本号为依据设置了 v1、v2 两个分组,运行上述代码,输出结果如下:
定义路由日志格式
Gin 框架默认路由日志格式如下:
[GIN-debug] POST /foo --> main.main.func1 (3 handlers)
[GIN-debug] GET /bar --> main.main.func2 (3 handlers)
[GIN-debug] GET /status --> main.main.func3 (3 handlers)
如果你想要记录指定格式(如 JSON、键值)的信息,可以通过 gin.DebugPrintRouteFunc
来定义这个格式,在下面这个例子中,我们将通过标准日志包记录所有路由信息,你也可以根据需要自定义日志格式:
func main() {
r := gin.Default()
// 默认路由输出格式
gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
log.Printf("endpoint %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
}
r.POST("/foo", func(c *gin.Context) {
c.JSON(http.StatusOK, "foo")
})
r.GET("/bar", func(c *gin.Context) {
c.JSON(http.StatusOK, "bar")
})
r.GET("/status", func(c *gin.Context) {
c.JSON(http.StatusOK, "ok")
})
// Listen and Server in http://0.0.0.0:8080
r.Run()
}
启动该服务器,输出结果如下:
接受参数
取得 Get 请求参数
URL 参数通过 DefaultQuery 或 Query 方法获取
// url 为 http://localhost:8080/welcome?name=alsritter
// 输出 Hello alsritter
// url 为 http://localhost:8080/welcome时
// 输出 Hello Guest
router.GET("/welcome", func(c *gin.Context) {
name := c.DefaultQuery("name", "Guest") //可设置默认值
// 是 c.Request.URL.Query().Get("lastname") 的简写
lastname := c.Query("lastname")
fmt.Println("Hello %s", name)
})
RestFul 风格参数
router.GET("/string/:name", func(c *gin.Context) {
name := c.Param("name")
fmt.Printf("Hello %s", name)
})
直接绑定结构体
type Student struct {
ID string `uri:"id" binding:"required,uuid"`
Name string `uri:"name" binding:"required"`
}
func main() {
route := gin.Default()
route.GET("/:name/:id", func(c *gin.Context) {
var student Student
// 将路由参数绑定到结构体中
if err := c.ShouldBindUri(&student); err != nil {
c.JSON(400, gin.H{"msg": err})
return
}
c.JSON(200, gin.H{"name": student.Name, "uuid": student.ID})
})
route.Run(":8088")
}
取得 POST 表单参数
//form
router.POST("/form", func(c *gin.Context) {
// 解析表单字段 type,如果为空的话使用默认值 alert
ty := c.DefaultPostForm("type", "alert")//可设置默认值
msg := c.PostForm("msg")
title := c.PostForm("title")
fmt.Println("type is %s, msg is %s, title is %s", ty, msg, title)
})
或者直接绑定结构体
type Person struct {
Name string `form:"name"`
Address string `form:"address"`
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
}
func startPage(c *gin.Context) {
var person Person
// If `GET`, only `Form` binding engine (`query`) used.
// If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`).
// See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48
if c.ShouldBind(&person) == nil {
log.Println(person.Name)
log.Println(person.Address)
log.Println(person.Birthday)
}
c.String(200, "Success")
}
func main() {
route := gin.Default()
// GET 请求
route.GET("/testing", startPage)
// POST 请求
route.POST("/testing", startPage)
route.Run(":8085")
}
只绑定查询字符串
使用 ShouldBindQuery 方法将只绑定查询字符串(就是 url 里面的参数),而忽略 POST 表单数据:
type Person struct {
Name string `form:"name"`
Address string `form:"address"`
}
func main() {
route := gin.Default()
route.Any("/testing", startPage)
route.Run(":8085")
}
func startPage(c *gin.Context) {
var person Person
if c.ShouldBindQuery(&person) == nil {
log.Println("====== Only Bind By Query String ======")
log.Println(person.Name)
log.Println(person.Address)
}
c.String(200, "Success")
}
嵌套结构体绑定表单
package main
import (
"github.com/gin-gonic/gin"
)
// 最基本的结构体
type StructA struct {
FieldA string `form:"field_a"`
}
// 嵌套结构体的结构体
type StructB struct {
NestedStruct StructA
FieldB string `form:"field_b"`
}
// 嵌套结构体指针的结构体
type StructC struct {
NestedStructPointer *StructA
FieldC string `form:"field_c"`
}
// 嵌套匿名结构体的结构体
type StructD struct {
NestedAnonymousStruct struct {
FieldX string `form:"field_x"`
}
FieldD string `form:"field_d"`
}
// 返回 StructB
func GetDataB(c *gin.Context) {
var b StructB
// 读取请求数据并写入结构体b
c.Bind(&b)
// 返回 JSON 格式响应
c.JSON(200, gin.H{
"a": b.NestedStruct,
"b": b.FieldB,
})
}
// 返回 StructC
func GetDataC(c *gin.Context) {
var b StructC
c.Bind(&b)
c.JSON(200, gin.H{
"a": b.NestedStructPointer,
"c": b.FieldC,
})
}
// 返回 StructD
func GetDataD(c *gin.Context) {
var b StructD
c.Bind(&b)
c.JSON(200, gin.H{
"x": b.NestedAnonymousStruct,
"d": b.FieldD,
})
}
func main() {
r := gin.Default()
r.GET("/getb", GetDataB)
r.GET("/getc", GetDataC)
r.GET("/getd", GetDataD)
r.Run()
}
通过 curl 发起请求:
PureJSON 编码特殊字符
通常,JSON 会通过 Unicode 编码特殊的 HTML 字符,比如将 <
替换成 \u003c
,这样一来会影响可读性,如果你想要按照字面意义编码这些字符,可以使用 PureJSON 方法:
func main() {
r := gin.Default()
// Serves unicode entities
r.GET("/json", func(c *gin.Context) {
c.JSON(200, gin.H{
"html": "<b>Hello, world!</b>",
})
})
// Serves literal characters
r.GET("/purejson", func(c *gin.Context) {
c.PureJSON(200, gin.H{
"html": "<b>Hello, world!</b>",
})
})
// listen and serve on 0.0.0.0:8080
r.Run(":8080")
}
运行上述代码,在终端窗口通过 curl 进行测试:
获取请求头
func Index(c *gin.Context) {
ua := c.GetHeader("User-Agent")
// do something ...
}
上传文件
router.POST("/upload", func(c *gin.Context) {
file, header, err := c.Request.FormFile("upload")
filename := header.Filename
fmt.Println(header.Filename)
out, err := os.Create("./tmp/" + filename + ".png")
if err != nil {
log.Fatal(err)
}
defer out.Close()
_, err = io.Copy(out, file)
if err != nil {
log.Fatal(err)
}
})
响应数据
最简单的响应:
c.String(http.StatusOK, "some string")
响应基本数据类型
JSON/XML/YAML响应:
r.GET("/moreJSON", func(c *gin.Context) {
// You also can use a struct
var msg struct {
Name string `json:"user" xml:"user"`
Message string
Number int
}
msg.Name = "Lena"
msg.Message = "hey"
msg.Number = 123
// 注意 msg.Name 变成了 "user" 字段
// 以下方式都会输出 : {"user": "Lena", "Message": "hey", "Number": 123}
c.JSON(http.StatusOK, gin.H{"user": "Lena", "Message": "hey", "Number": 123})
c.XML(http.StatusOK, gin.H{"user": "Lena", "Message": "hey", "Number": 123})
c.YAML(http.StatusOK, gin.H{"user": "Lena", "Message": "hey", "Number": 123})
c.JSON(http.StatusOK, msg)
c.XML(http.StatusOK, msg)
c.YAML(http.StatusOK, msg)
})
响应视图(HTML 模板)
不推荐这种方式响应页面,因为现在都是前后端分离的,但是某些场景(懒得写前端服务),可以使用这个
先要使用 LoadHTMLTemplates()
方法来加载模板文件
下面展示模板文件传参:
func main() {
router := gin.Default()
//加载模板
router.LoadHTMLGlob("templates/*")
//router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
//定义路由
router.GET("/index", func(c *gin.Context) {
//根据完整文件名渲染模板,并传递参数
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "Main website",
})
})
router.Run(":8080")
}
模板结构定义
<html>
<h1>
{{ .title }}
</h1>
</html>
不同文件夹下模板名字可以相同,此时需要 LoadHTMLGlob()
加载两层模板路径
router.LoadHTMLGlob("templates/**/*")
router.GET("/posts/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{
"title": "Posts",
})
c.HTML(http.StatusOK, "users/index.tmpl", gin.H{
"title": "Users",
})
}
模板文件
<!-- 注意开头 define 与结尾 end 不可少 -->
{{ define "posts/index.tmpl" }}
<html><h1>
{{ .title }}
</h1>
</html>
{{ end }}
重定向
Gin 框架支持内部和外部重定向:
func main() {
r := gin.Default()
// HTTP 重定向
r.GET("/test", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "https://www.baidu.com/")
})
// 路由重定向
r.GET("/test1", func(c *gin.Context) {
c.Request.URL.Path = "/test2"
r.HandleContext(c)
})
r.GET("/test2", func(c *gin.Context) {
c.JSON(200, gin.H{"hello": "world"})
})
r.Run()
}
AsciiJSON 转码
使用 AsciiJSON 方法可以生成只包含 ASCII 字符的 JSON 格式数据,对于非 ASCII 字符会进行转义:
func main() {
r := gin.Default()
r.GET("/asciiJSON", func(c *gin.Context) {
data := map[string]interface{}{
"lang": "Gin框架",
"tag": "<br/>",
}
// 输出: {"lang":"Gin\u6846\u67b6","tag":"\u003cbr\u003e"}
c.AsciiJSON(http.StatusOK, data)
})
// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}
下载文件
func main() {
router := gin.Default()
router.GET("/dataFromReader", func(c *gin.Context) {
response, err := http.Get("https://go.dev/images/go-logo-white.svg")
if err != nil || response.StatusCode != http.StatusOK {
c.Status(http.StatusServiceUnavailable)
return
}
reader := response.Body
contentLength := response.ContentLength
contentType := response.Header.Get("Content-Type")
extraHeaders := map[string]string{
"Content-Disposition": `attachment; filename="go-logo-white.svg"`,
}
c.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders)
})
router.Run(":8088")
}
访问 /dataFromReader
路由会下载图片到本地。
控制器
异步处理请求
goroutine 机制可以方便地实现异步处理,在中间件或处理器中开启新的协程时,不应该在其中使用原生的上下文对象,而应该使用它的只读副本:
func main() {
r := gin.Default()
//1. 异步
r.GET("/long_async", func(c *gin.Context) {
// goroutine 中只能使用只读的上下文 c.Copy()
cCp := c.Copy()
go func() {
time.Sleep(5 * time.Second)
// ⚠️ 注意这里使用的是上下文对象的只读副本 "cCp"
log.Println("Done! in path " + cCp.Request.URL.Path)
}()
})
//2. 同步的写法
r.GET("/long_sync", func(c *gin.Context) {
time.Sleep(5 * time.Second)
// 注意可以使用原始上下文
log.Println("Done! in path " + c.Request.URL.Path)
})
// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}
之所以使用副本是为了避免污染其中的数据,context 参数传递的是全局的
使用中间件
func main() {
// 创建一个默认不使用任何中间件的路由器
r := gin.New()
// 设置全局中间件
// Logger 中间件会记录日志到 gin.DefaultWriter,即使你设置了 GIN_MODE=release 这个环境变量
// 默认情况下 gin.DefaultWriter = os.Stdout(控制台标准输出)
r.Use(gin.Logger())
// Recovery 中间件会从任意 panics 中恢复并且返回 500 响应(服务端错误)
r.Use(gin.Recovery())
// 设置路由中间件
// 路由中间件可以被添加到指定路由上,并且不限数量
r.GET("/benchmark", MyBenchLogger(), benchEndpoint)
// 为指定路由分组设置中间件
// 认证组
// authorized := r.Group("/", AuthRequired())
// 上面这行代码等同于下面下的代码:
authorized := r.Group("/")
// 下面为 `/` 前缀的路由分组设置中间件 AuthRequired,表示该分组下的路由用户认证后才能访问
authorized.Use(AuthRequired())
{
authorized.POST("/login", loginEndpoint)
authorized.POST("/submit", submitEndpoint)
authorized.POST("/read", readEndpoint)
// 嵌套分组
testing := authorized.Group("testing")
testing.GET("/analytics", analyticsEndpoint)
}
// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}
注意:
// 这种情况下默认会使用 Logger 和 Recovery 中间件
r := gin.Default()
自定义中间件
自定义一个中间件,通过 c.Next()
分隔请求前和请求后,注意在中间件中开启新的协程时,不应该在其中使用原生的上下文对象,而应该使用它的只读副本:
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()
// 设置 example 变量
c.Set("example", "12345")
// 请求之前...
c.Next()
// 请求之后...
latency := time.Since(t)
// 打印请求处理时间
log.Print(latency)
// 访问即将发送的响应状态码
status := c.Writer.Status()
log.Println(status)
}
}
func main() {
r := gin.New()
// 使用自定义的 Logger 中间件
r.Use(Logger())
// 定义路由
r.GET("/test", func(c *gin.Context) {
example := c.MustGet("example").(string)
// 打印 example 值 12345
log.Println(example)
})
// 监听 0.0.0.0:8080
r.Run(":8080")
}
打印效果
请求类型解析绑定
模型绑定可以将请求体绑定给一个类型,目前支持绑定的类型有 JSON, XML 和标准表单数据 (foo=bar&boo=baz)
。
要注意的是绑定时需要给字段设置绑定类型的标签 Tag。比如绑定 JSON 数据时,设置 json:"fieldname"
。 使用绑定方法时,Gin 会根据请求头中 Content-Type 来自动判断需要解析的类型。
如果明确绑定的类型,可以不用自动推断,而用 BindWith 方法。也可以指定某字段是必需的。如果一个字段被 binding:"required"
修饰而值却是空的,请求会失败并返回错误。
example:
// Binding from JSON
type Login struct {
User string `form:"user" json:"user" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}
func main() {
router := gin.Default()
// 绑定JSON的例子 ({"user": "manu", "password": "123"})
router.POST("/loginJSON", func(c *gin.Context) {
var json Login
if c.BindJSON(&json) == nil {
if json.User == "manu" && json.Password == "123" {
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
} else {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
}
}
})
// 绑定普通表单的例子 (user=manu&password=123)
router.POST("/loginForm", func(c *gin.Context) {
var form Login
// 根据请求头中 content-type 自动推断.
if c.Bind(&form) == nil {
if form.User == "manu" && form.Password == "123" {
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
} else {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
}
}
})
// 绑定多媒体表单的例子 (user=manu&password=123)
router.POST("/login", func(c *gin.Context) {
var form Login
// 可以显式声明来绑定多媒体表单:
// c.BindWith(&form, binding.Form)
// 或者使用自动推断:
if c.Bind(&form) == nil {
if form.User == "user" && form.Password == "password" {
c.JSON(200, gin.H{"status": "you are logged in"})
} else {
c.JSON(401, gin.H{"status": "unauthorized"})
}
}
})
// Listen and serve on 0.0.0.0:8080
router.Run(":8080")
}
表单验证
注册自定义的表单验证器,下面自定义了一个验证器 bookabledate
,并且 gtfield=CheckIn
来避免 CheckIn 大于 CheckOut
import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
"net/http"
"time"
)
// Booking 中包含了绑定的表单请求字段和验证规则
type Booking struct {
CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
}
var bookableDate validator.Func = func(fl validator.FieldLevel) bool {
date, ok := fl.Field().Interface().(time.Time)
if ok {
today := time.Now()
// 如果 date 在当前时间之前则校验失败
if today.After(date) {
return false
}
}
return true
}
func getBookable(c *gin.Context) {
var b Booking
if err := c.ShouldBindWith(&b, binding.Query); err == nil {
c.JSON(http.StatusOK, gin.H{"message": "预定日期有效!"})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
}
func main() {
route := gin.Default()
// 注册新的自定义验证规则
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("bookabledate", bookableDate)
}
route.GET("/bookable", getBookable)
route.Run(":8085")
}
启动服务器,测试结果如下:
# check_in 时间在当前时间之前
http://localhost:8085/bookable?check_in=2019-11-11&check_out=2021-11-12
{
"error": "Key: 'Booking.CheckOut' Error:Field validation for 'CheckOut' failed on the 'gtfield' tag"
}
# check_in 时间在 check_out 时间之前
http://localhost:8085/bookable?check_in=2022-11-11&check_out=2021-11-12
{
"error": "Key: 'Booking.CheckOut' Error:Field validation for 'CheckOut' failed on the 'gtfield' tag"
}
http://localhost:8085/bookable?check_in=2022-11-11&check_out=2023-11-12
{
"message": "预定日期有效!"
}
使用 BasicAuth 中间件
Basic Auth 是一种开放平台认证方式,简单的说就是需要你输入用户名和密码才能继续访问。 如果需要针对单个路由使用,在要在单路由中注册 BasicAuth 中间件即可。
// 使用BasicAuth中间件
func main(){
engine := gin.Default()
// 设置账号和密码,key:代表账号,value:代表密码
ginAccounts := gin.Accounts{
"user":"password",
"abc":"123",
}
// 注册路由和中间件
engine.GET("/test",gin.BasicAuth(ginAccounts), func(context *gin.Context) {
// 获取中间件BasicAuth
user := context.MustGet(gin.AuthUserKey).(string)
fmt.Println(user)
context.JSON(200,gin.H{"msg":"success"})
})
_ = engine.Run()
}
访问效果
绝大部分情况下,我们都是在路由组中使用 BasicAuth 中间件。
func RunUseBasicAuthWithGroup() {
engine := gin.Default()
// 注册路由组和中间件
userGroup := engine.Group("/user", gin.BasicAuth(gin.Accounts{
"abc": "123",
}))
userGroup.GET("info", func(context *gin.Context) {
context.JSON(200, gin.H{"msg": "user.info"})
})
}
为 Gin 框架编写测试用例
例如编写一个服务
package main
import "github.com/gin-gonic/gin"
func setupRouter() *gin.Engine {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
return r
}
func main() {
r := setupRouter()
r.Run(":8088")
}
对应的测试代码,需要和待测试代码位于同一目录下
package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestPingRoute(t *testing.T) {
router := setupRouter()
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/ping", nil)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, "pong", w.Body.String())
}
Reference
Gin Web Framework Document Gin框架如何处理session Gin 使用教程 gin牛逼的context Gin框架(九):BasicAuth授权认证中间件使用